#!/usr/bin/perl -w   
#
# $Id: netops-backup.pl 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $
# Copyright (c) 2009 Prabhu Gurumurthy <pgurumur@gmail.com>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#       

use strict;
use warnings;
use Digest::SHA;
use Getopt::Long;
use File::Copy;
use Mail::Sendmail 0.75;
use POSIX;
use Socket;
use Sys::Syslog;
use Sys::Syslog qw(:DEFAULT setlogsock);
# use threads;

use ConfigSQL;
use ConfigCopy;
use ConfigRCS;

# Configure Getopt as it says,
Getopt::Long::Configure("default", "bundling");

# Similar to basename
push my @prog, split(/\//, $0);
my $program = $prog[$#prog];

# See whether we have any arguments
my $arg = int(scalar(@ARGV));

# Where the getopt options are stored, may move it to main function
my %opt = ();

# syslog variables, to see whether syslog is being used or not
my $syslog = 0;

# debug variable, to see whether debug is being used or not
my $debug = 0;

# Who is the contact person for a system
my $mail_contact = undef;

# What is the ssh script that I am going to try to use
my $ssh_script = "/usr/ssn/bin/asabackup.py";

# What is my version, if you make more changes, bump up the version
my $ver = 1.0;

# My RCS handle
my $rcs = undef;

# Global db parameters
my $db = undef;
my $dbHost = undef;
my $dbUser = undef;
my $dbPass = undef;

# Function would either print the message or send an email out before
# closing and open file handles
sub fatal {
   my ($email, $message) = @_;
   # $email is treated as boolean, 1 being send, 0 and others being do not send

   if (defined($message)) {
      # If email is present,
      if ($email == 1) {
         my @mesg = ();
         push @mesg, $message;
         my ($sys, $node, $release, $ver, $machine) = POSIX::uname();
         if (defined($mail_contact)) {
            my $subject = sprintf("%s: encountered problem backup up on %s", 
               $program, $node);
            email($mail_contact, $subject, @mesg);
         }
      }
      else {
         printf("%s: %s\n", $program, $message);
      }
      
      # Same with syslog, if open close it, as we are about to exit
      if ($syslog eq 1) {
         syslogClose();
      }

   }

   # Exit with value 1
   exit(1);
}

# Send out a warning, in an email or print it, need to move the print to
# syslog, may be next version
sub warning {
   my ($email, $message) = @_;

   if (defined($message)) {

      if ($email == 1) {
         my @mesg = ();
         push @mesg, $message;
         my ($sys, $node, $release, $ver, $machine) = POSIX::uname();
         if (defined($mail_contact)) {
            my $subject = sprintf("%s: encountered backup problem on %s",
               $program, $node);
            email($mail_contact, $subject, @mesg);
         }
      }
      else {
         printf("%s: %s\n", $program, $message);
      }
   }
}

# Send mail, (as the function suggests) 
sub email {
   my ($contact, $subject, @body) = @_;

   my %mail = ();
   my ($sys, $node, $release, $ver, $machine) = POSIX::uname();

   if (defined($contact)) {
      my $from = 'netreports@silverspringnet.com';
      %mail = ( 
         from => $from, 
         to => $contact, 
         subject => $subject); 
      
      $mail{body} = <<BODY_END;
@body
BODY_END
      sendmail(%mail) or fatal(0, "$Mail::Sendmail::error");
   }
}

sub syslogOpen {

   logging("in syslogOpen function");
   if ($syslog eq 0) {
      setlogsock("unix");
      openlog("$program: $$", "cons,pid", "user");
      $syslog = 1;
   }
}

sub syslogClose {

   logging("in syslogClose function");
   if ($syslog eq 1) {
      closelog();
   }
}

sub logging {
   my ($message) = @_;

   if ($debug eq 1) {
      if ($syslog eq 1) {
         mesglog("debug", $message);
      }
      else {
         printf("%s\n", $message);
      }
   }
}

# Wrapper function syslog
sub mesglog {
   my ($facility, $message) = @_;

   # If facility is not defined, we default to info
   if (defined($message)) {
      if (!defined($facility) or ($facility eq 0)) {
         $facility = "info";
      }

      syslog($facility, "%s\n", $message);
   }
}

# Open SQL handle
sub sqlOpen {
   my ($hostname, $database, $username, $password) = @_;

   logging("in sqlOpen function");
   my $retval = 1;
   my $sql = new ConfigSQL($database, $hostname, $username, $password);
   return $sql
}

sub sqlClose {
   my ($handle) = @_;

   logging("in sqlClose function");
   if (defined($handle)) {
      $handle->close();
   }
}

sub getSnmpInfo {
   my ($id, $sql) = @_;

   my @retval = ();
   my $snmpMaxCol = 9;
   my $sqlQuery = sprintf("select * from snmp_info where id = %d;", $id);
   my @temp = $sql->execute($sqlQuery);

   for (my $ix = 0; $ix < $snmpMaxCol; $ix++) {
      $retval[$ix] = $temp[0][$ix];
   }

   return @retval;
}

sub GetSSHInfo {
   my ($id, $sql) = @_;

   my @retval = ();
   my $sshMaxCol = 6;
   my $sqlQuery = sprintf("select * from ssh_info where id = %d;", $id);
   my @temp = $sql->execute($sqlQuery);

   for (my $ix = 0; $ix < $sshMaxCol; $ix++) {
      $retval[$ix] = $temp[0][$ix];
   }

   return @retval;
}

sub GetDevInfo {
   my ($id, $sql) = @_;

   my @sqlElements = ();
   my @retval = ();
   if (defined($id) and defined($sql)) {
      if ($id gt 0) {
         my $sqlQuery = sprintf(
            "select site.name, rr.name, zone.mname, community, product.name, 
            ipblock.address from device left join site on site.id = device.site 
            left join product on product.id = device.product 
            left join ipblock on ipblock.id = device.snmp_target 
            left join rr on rr.id = device.name 
            left join zone on zone.id = rr.zone where device.id = %d;", $id);

         my @sqlElements = $sql->execute($sqlQuery);
         my $sqlcol = 6;
         for (my $ix = 0; $ix < $sqlcol; $ix++) {
            if ($ix eq 5) {
               push @retval, inet_ntoa(pack("N", $sqlElements[0][$ix]));
            }
            else {
               push @retval, $sqlElements[0][$ix];
            }
         }
      }
   }

   return @retval;
}

sub rcs_backup {
   my ($system, $new_conf) = @_;

   my $digest = undef;
   my $now = localtime();
   my $bindir = "/usr/bin";
   my $backup_dir = "/usr/ssn/etc/netops";
   my $backup_rcs = sprintf("%s/RCS", $backup_dir);

   logging("time: $now");

   my $rcs = new ConfigRCS($bindir, $backup_rcs, $backup_dir);

   my $uid = 0;
   my $gid = 2002;
   my $mode = 0440;

   if (-f $new_conf && -s $new_conf) {
      push my @temp, split(/\//, $new_conf);
      my $sys_conf = $temp[$#temp];

      chdir($backup_dir);
      if (-f $sys_conf) {
         logging("original system config: $sys_conf");
         logging("newly backed up system config: $new_conf");

         $rcs->checkout($sys_conf);
         move($new_conf, $sys_conf);

         my @changed = $rcs->diff();
         my $message = sprintf("automatic checkin of %s config file on %s",
            $system, $now);

         logging("rcs automatic checkin message: $message");
         $rcs->checkin($sys_conf, $message);
         chown $uid, $gid, $sys_conf;
         chmod $mode, $sys_conf;

         if (@changed) {
            logging("changed content: @changed");
            my $subject = sprintf("configuration changed on %s", $system);
            email($mail_contact, $subject, @changed);
         }

         if ( -f $sys_conf ) { 
            my $ctx = Digest::SHA->new("sha1");
            open(FH, $sys_conf) or warning(1, "cannot open $sys_conf: $!");
            $ctx->addfile(*FH);
            close(FH);
            $digest = $ctx->hexdigest;
            logging("digest: $digest");
         }

      }
      else {
         warning(1, "cannot find configuration file to RCS backup for $system");
      }
   }
   else {
      warning(1, 
         "cannot find the backup config, or backup config size is zero");
   }

   return $digest;
}

sub backup {
   my ($id, $active, $dev_id, $tftp, $snmp, $ssh, $digest, $sleep, $contact,
      $descr, $update) = @_;
   my $system = undef;

   my $tid = threads->tid();
   logging("in function backup");
   logging("thread id: $tid");

   my $db = sqlOpen($dbHost, $db, $dbUser, $dbPass);
   if (defined($db)) {
      if (!defined($contact)) {
         warning(0, "undefined contact email address, bailing...")
      }
      else {
         $mail_contact = $contact;
         logging("contact email address: $contact");

         if ($active == 1) {
            my ($site, $name, $domain, $snmpRo, $model, $device) = GetDevInfo(
               $dev_id, $db);
            
            $system = sprintf("%s.%s", $name, $domain);
            # my ($snmpId, $snmpVer, $snmpRo, $snmpRw, $snmpUser, $snmpEngine, 
            #   $snmpAuth, $snmpPass, $snmpUpdate) = getSnmpInfo($snmp, $db);

            if (!defined($sleep)) {
               $sleep = 60;
            }

            $tftp = inet_ntoa(pack("N", $packed_ip));

            logging("tftp server: $tftp");
 
            my $config = new ConfigCopy;
            my $retval = $config->set_params(
               $device, $snmpRw, undef, $tftp, $sleep);
            if ($retval != 0) {
               logging("set_params failed for $system");
               warning(1, $config->get_error());
               $config->close();
            }
            else {
               logging("backing up $system");
               my $descr = $config->get_device();
               logging("$system description: $descr");
               logging("using ssh script: $ssh_script");
               if ($descr eq "asa") {
                  if ($ssh == 1) {
                     my ($sshId, $user, $passwd, $port, $enable, 
                        $time) = GetSSHInfo($ssh, $db);
                     
                     logging("ssh user for $system: $user");
                     if (! -f $ssh_script || ! -x $ssh_script ) {
                        warning(1, "cannot stat $ssh_script");
                     }
                     else { 
                        $config->ssh_connect(
                           $ssh_script, $user, $passwd, $port);
                     }
                  }
                  else {
                     warning(1, "system is ASA, no ssh table defined");
                  }
               }
               
               if ($config->get_config() == 1) {
                  warning(1, $config->get_error());
               }

               my $config_file = $config->get_config_file();
               $config->close();

               push @temp, split(/\//, $config_file);
               my $new_conf = sprintf("%s/%s.cfg", $temp[0], $system);
               move($config_file, $new_conf);
               logging("config file: $new_conf");

               my $digest = rcs_backup($system, $new_conf);
               if (defined($digest)) {
                  my $sqlQuery = "update config_backup set ";
                  $sqlQuery .= sprintf("digest = \"%s\" where id = %d;",
                     $digest, $id);
                  $db->insert($sqlQuery);
               }
            }
         }
         else {
            warning(0, "$system is not marked active, skipping");
         }
      }

      sqlClose($db);
   }
}

sub main {
   GetOptions(\%opt, 
      "help|?", "debug|d", "syslog|s", "script|S=s", 
      "conf|C=s", "backup|b=s") or usage();
   usage() if $opt{help};

   if ($opt{syslog}) {
      logging("syslog enabled!");
      syslogOpen();
   }

   if ($opt{debug}) {
      $debug = 1;
   }

   if ($opt{script}) {
      $ssh_script = $opt{script};
   }

   if ($opt{conf}) {

      logging("db configuration file: $opt{conf}");

      my $database = undef;
      my $hostname = undef;
      my $username = undef;
      my $password = undef;

      my $count = 0;

      open(FH, $opt{conf}) or fatal("cannot open $opt{conf}: $!");
      while (<FH>) {
         $count++;
         next if /^\s*#/;
         next if /^\s*$/;

         chomp(my $line = $_);

         if ($line =~ /^\s*(db|host|user|pass)\s+(.*)\s*$/) {
            if ($1 eq "db") {
               chomp($database = $2);
            }
            elsif ($1 eq "host") {
               chomp($hostname = $2);
            }
            elsif ($1 eq "user") {
               chomp($username = $2);
            }
            elsif ($1 eq "pass") {
               chomp($password = $2);
            }
            else {
               close(FH);
               fatal("undefined token $1 in line $count");
            }
         }
      }

      close(FH);

      my $sql = undef;
      if (defined($hostname) and defined($database) and defined($username)
            and defined($password)) {
         logging("database: $database");
         logging("hostname: $hostname");
         logging("username: $username");

         $sql = sqlOpen($hostname, $database, $username, $password);
         if (!defined($sql)) {
            fatal(1, "cannot open SQL handle to start backing up configs");
         }

         $db = $database;
         $dbHost = $hostname;
         $dbUser = $username;
         $dbPass = $password;

         logging("SQL handle opened to $hostname");
      }

      my $sqlQuery = "select * from config_backup;";
      my @retval = $sql->execute($sqlQuery);
      foreach (@retval) {
         my $thread = threads->create(\&backup, @$_);
      }

      foreach my $thread (threads->list()) {
         my $retval = $thread->join();
      }

      sqlClose($sql);
   }

   if ($opt{syslog}) {
      syslogClose();
   }
}

sub usage {
   printf("%s version %0.01f\n", $program, $ver);
   printf("%s: [ options ]\n", $program);
   printf("options: \n");
   printf(" -C | --conf       [ Database configuration file ]\n");
   printf(" -b | --backup     [ Tftp backup configuration directory ]\n");
   printf(" -h | --help       [ Prints this help ]\n");
   printf(" -s | --syslog     [ Enable syslog for logging ]\n");
   printf(" -d | --debug      [ Enable debugging ]\n");
   printf(" -S | --script     [ Script for SSH based backup ]\n");
   exit(1);
}

if ($arg le 1) {
   usage();
}
else {
   main();
}
